package com.mattgarner.jaddas.node.data;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;

import com.mattgarner.jaddas.common.ColumnType;
import com.mattgarner.jaddas.common.ResultSetException;
import com.mattgarner.jaddas.common.ResultSetHeader;


public class TableHeader {
	private ArrayList<String> columnNames;
	private ArrayList<Byte> columnTypes;
	private ArrayList<Byte> columnIndexModel;
	private ArrayList<Byte> columnRules;
	
	private String tableName;
	
	private byte[] headerBytes;
	private boolean headerBytesValid = false;
	private boolean headerBytesParsed = false;
	
	private int primaryKey = -1;
	private int autoIncrementNextValue = 0;
	
	private int cursor = 0;
	
	// some constants
	
	public final static byte COLUMN_RULE_NULL = 1;
	public final static byte COLUMN_RULE_NOTNULL = 2;
	
	// constants
	public static final byte IDX_NONE = 0;
	public static final byte IDX_INT = 1;
	public static final byte IDX_STRING = 2;
	
	
	public TableHeader(String localTableName) {
		columnNames = new ArrayList<String>();
		columnTypes = new ArrayList<Byte>();
		columnIndexModel = new ArrayList<Byte>();
		columnRules = new ArrayList<Byte>();
		headerBytesValid = false;
		headerBytesParsed = true;
		tableName = localTableName;
	}
	
	public TableHeader(byte[] hdrBytes) {
		columnNames = new ArrayList<String>();
		columnTypes = new ArrayList<Byte>();
		columnIndexModel = new ArrayList<Byte>();
		columnRules = new ArrayList<Byte>();
		headerBytes = hdrBytes;
		headerBytesValid = true;
		headerBytesParsed = false;
	}
	
	public TableHeader(byte[] hdrBytes, boolean parse) throws TableException {
		this(hdrBytes);
		if(parse) { parseHeaderBytes(); }
	}
	
	public final void setColumn(int index, byte columnType, String columnName) throws TableException {
		parseHeaderBytes();
		if(index >= columnTypes.size()) { 
			columnTypes.add(index, columnType);
			columnNames.add(index, columnName);
			columnIndexModel.add(index, IDX_NONE);
			columnRules.add(index, COLUMN_RULE_NULL);
		} else {
			columnTypes.set(index, columnType);
			columnNames.set(index, columnName);
		}
		if(index > cursor) { cursor = index + 1; }
		headerBytesValid = false;
	}
	
	public final void addColumn(byte columnType, String columnName) throws TableException {
		setColumn(cursor, columnType, columnName);
		cursor++;
	}
	
	public final void addIntColumn(String columnName) throws TableException {
		addColumn(ColumnType.COLUMN_TYPE_INT, columnName);
	}
	
	public final void addShortColumn(String columnName) throws TableException {
		addColumn(ColumnType.COLUMN_TYPE_SHORT, columnName);
	}
	
	public final void addLongColumn(String columnName) throws TableException {
		addColumn(ColumnType.COLUMN_TYPE_LONG, columnName);
	}
	
	public final void addVarcharColumn(String columnName) throws TableException {
		addColumn(ColumnType.COLUMN_TYPE_VARCHAR, columnName);
	}
	
	public final void addBinaryColumn(String columnName) throws TableException {
		addColumn(ColumnType.COLUMN_TYPE_BINARY, columnName);
	}
	
	public final void setColumnIndexModel(int index, byte indexModel) {
		columnIndexModel.set(index, indexModel);
	}
	
	public final void setColumnRule(int index, byte columnRule) {
		columnRules.set(index, columnRule);
	}
	
	
	public final void setPrimaryKey(int index) throws TableException {
		if(index <= cursor) {
			byte colType = columnTypes.get(index);
			
			if(primaryKey > -1) {
				throw new TableException("Primary key has already been set on column ("+primaryKey+")");
			}
			
			byte newColumnType = ColumnType.COLUMN_TYPE_UNDEFINED;
			switch (colType) {
				case ColumnType.COLUMN_TYPE_INT: newColumnType = ColumnType.COLUMN_TYPE_PRIKEY_INT; break; 
				case ColumnType.COLUMN_TYPE_SHORT: newColumnType = ColumnType.COLUMN_TYPE_PRIKEY_SHORT; break;
				case ColumnType.COLUMN_TYPE_LONG: newColumnType = ColumnType.COLUMN_TYPE_PRIKEY_LONG; break;
				case ColumnType.COLUMN_TYPE_VARCHAR: newColumnType = ColumnType.COLUMN_TYPE_PRIKEY_VARCHAR; break;
			}
			
			if(newColumnType == ColumnType.COLUMN_TYPE_UNDEFINED) {
				throw new TableException("Unsupported primary key column type.");
			}
			 
			columnTypes.set(index, newColumnType);
			primaryKey = index;
		} else {
			throw new TableException("setKey: index out-of-bounds: " + index);
		}
	   
	}
	
	public final void setAutoIncrementKey(int index) throws TableException {
		if(index <= cursor) {
			byte colType = columnTypes.get(index);
			if((colType == ColumnType.COLUMN_TYPE_PRIKEY_INT) || 
				(colType == ColumnType.COLUMN_TYPE_PRIKEY_SHORT) ||
				(colType == ColumnType.COLUMN_TYPE_PRIKEY_LONG)) {
					columnTypes.set(index, (byte)(colType + 10));
			} else {
				throw new TableException("setAutoIncrementKey: can only set AUTOINCREMENT on INT, SHORT or LONG PRIMARY KEYS.");
			}
		} else {
			throw new TableException("setAutoIncrementKey: index out-of-bounds: " + index);
		}
	   
	}
	
	public final byte[] serializeToByteArray() {
		if(headerBytesValid && headerBytesParsed) {  // if headerBytes is still valid, don't recreate them
			return headerBytes;
		}
		
		ByteArrayOutputStream baosHeader = new ByteArrayOutputStream();
		DataOutputStream header = new DataOutputStream(baosHeader);
		try { 
			header.writeByte((byte)columnTypes.size());
			header.writeByte((byte)primaryKey);
			for(int a=0; a<columnTypes.size(); a++) {
				header.writeByte(a);
				header.writeByte(columnTypes.get(a));
				header.writeByte(columnIndexModel.get(a));
				header.writeByte(columnRules.get(a));
				header.writeUTF(columnNames.get(a));
			}
			header.close();
			baosHeader.close();
			headerBytes = baosHeader.toByteArray();
			headerBytesValid = true;
			return headerBytes;
		} catch(IOException e) {
			// can this ever happen on a ByteArrayOutputStream?
		}
		return null;
	}
	
	public void parseHeaderBytes() throws TableException {
		if(headerBytesParsed) { return; }
		ByteArrayInputStream baisHeader = new ByteArrayInputStream(headerBytes);
		DataInputStream header = new DataInputStream(baisHeader);
		
		try {
			int headerSize = (int)header.readByte();
			primaryKey = (int)header.readByte();
			
			columnNames = new ArrayList<String>();
			columnTypes = new ArrayList<Byte>();
			columnIndexModel = new ArrayList<Byte>();
			columnRules = new ArrayList<Byte>();
			
			for(int a = 0; a<headerSize; a++) {
				byte colIndex = header.readByte();
				if((int)colIndex != a) {
					throw new TableException("Column header out-of-order.");
				}
				
				columnTypes.add(a, header.readByte());
				columnIndexModel.add(a, header.readByte());
				columnRules.add(a, header.readByte());
				columnNames.add(a, header.readUTF());
			}
			headerBytesValid = true;
			headerBytesParsed = true;
		} catch(Exception e) {
		  throw new TableException("parseHeaderBytes: " + e.getMessage());
		}		
	}
	
	public final int getPrimaryKey() {
		return primaryKey;
	}
	
	public final int getColumnCount() throws TableException {
		parseHeaderBytes();
		return columnTypes.size();
	}
	
	public final String getColumnTypesString() {
		return columnTypes.toString();
	}
	
	public final String getColumnName(int index) {
		return columnNames.get(index);
	}
	
	public final byte getColumnType(int index) throws TableException {
		try { 
			return columnTypes.get(index);
		} catch(IndexOutOfBoundsException e) {
			throw new TableException("getColumnType: index out-of-bounds");
		}
	}
	
	public final byte getColumnBasicType(int index) throws TableException {
		try { 
			byte colType = columnTypes.get(index);
			if((colType == ColumnType.COLUMN_TYPE_INT) ||
				(colType == ColumnType.COLUMN_TYPE_PRIKEY_INT) ||
				(colType == ColumnType.COLUMN_TYPE_PRIKEY_INT_AUTOINCREMENT)) {
					return ColumnType.COLUMN_TYPE_INT;
			} else if((colType == ColumnType.COLUMN_TYPE_SHORT) ||
					(colType == ColumnType.COLUMN_TYPE_PRIKEY_SHORT) ||
					(colType == ColumnType.COLUMN_TYPE_PRIKEY_SHORT_AUTOINCREMENT)) {
						return ColumnType.COLUMN_TYPE_SHORT;
			} else if((colType == ColumnType.COLUMN_TYPE_LONG) ||
					(colType == ColumnType.COLUMN_TYPE_PRIKEY_LONG) ||
					(colType == ColumnType.COLUMN_TYPE_PRIKEY_LONG_AUTOINCREMENT)) {
						return ColumnType.COLUMN_TYPE_LONG;
			} else if((colType == ColumnType.COLUMN_TYPE_VARCHAR) ||
					(colType == ColumnType.COLUMN_TYPE_PRIKEY_VARCHAR)) {
						return ColumnType.COLUMN_TYPE_VARCHAR;
			} else {
				return colType;
			}
		} catch(IndexOutOfBoundsException e) {
			throw new TableException("getColumnType: index out-of-bounds");
		}
	}
	
	public final int getColumnIndexByName(String columnName) {
		for(int a=0; a<columnNames.size(); a++) {
			if(columnNames.get(a).matches(columnName)) {
				return a;
			}
		}
		return -1;
	}
	
	public final String getColumnTypeString(int index) throws TableException {
		byte colType = this.getColumnBasicType(index);
		if(colType == ColumnType.COLUMN_TYPE_INT) {
			return "INT";
		} else if(colType == ColumnType.COLUMN_TYPE_SHORT) {
			return "SHORT";
		} else if(colType == ColumnType.COLUMN_TYPE_LONG) {
			return "LONG";
		} else if(colType == ColumnType.COLUMN_TYPE_VARCHAR) {
			return "VARCHAR";
		} else if(colType == ColumnType.COLUMN_TYPE_BINARY) {
			return "BINARY";
		} else {
			return "UNKNOWN";
		}
	}
	
	public final void setNextAutoIncrementValue(int autoIncrement) {
		autoIncrementNextValue = autoIncrement;
	}
	
	public final int getNextAutoIncrementValue() {
		return autoIncrementNextValue++;
	}
	
	public final ResultSetHeader convertToResultSetHeader(String stringEncoding) throws TableException {
		ResultSetHeader newRSHeader = new ResultSetHeader(stringEncoding);
		for(int a=0; a<columnNames.size(); a++) {
			byte colType = getColumnBasicType(a);
			try { 
				if(colType == ColumnType.COLUMN_TYPE_INT) {
					newRSHeader.addIntColumn(columnNames.get(a));
				} else if(colType == ColumnType.COLUMN_TYPE_SHORT) {
					newRSHeader.addShortColumn(columnNames.get(a));
				} else if(colType == ColumnType.COLUMN_TYPE_LONG) {
					newRSHeader.addLongColumn(columnNames.get(a));
				} else if(colType == ColumnType.COLUMN_TYPE_VARCHAR) {
					newRSHeader.addStringColumn(columnNames.get(a));
				} else if(colType == ColumnType.COLUMN_TYPE_BINARY) {
					newRSHeader.addStringColumn(columnNames.get(a));
				}
			} catch(ResultSetException e) {
				throw new TableException("convertToResultSetHeader: ResultSetException: " + e.getMessage());
			}
		}
		return newRSHeader;
	}
}
